/******************************************************************************

    Simple Authentication and Authorisation System (SAAS)

    host.c

    Copyright (C) 2011  Josh Goes


    This program is free software: you can redistribute it and/or modify
    it under the terms of the GNU General Public License as published by
    the Free Software Foundation, either version 3 of the License, or
    (at your option) any later version.

    This program is distributed in the hope that it will be useful,
    but WITHOUT ANY WARRANTY; without even the implied warranty of
    MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
    GNU General Public License for more details.

    You should have received a copy of the GNU General Public License
    along with this program.  If not, see <http://www.gnu.org/licenses/>.

    If you require a mailing address for this software, please send your
    mail to the following address:
        SAAS
        GPO Box 2973
        BRISBANE QLD 4001
        AUSTRALIA

******************************************************************************/



#include <stdlib.h>
#include <netdb.h>
#include <netinet/in.h>
#include <sys/socket.h>
#include <string.h>
#include <time.h>
#include <arpa/inet.h>

#include "saas-build.h"


void insert_host( saas_t *inst , saas_host_t *parent_host , int addr_fam , void *addr );




void host_order( saas_t *inst )
{
	logm( inst , LOG_DEBUG , "host_order(): starting" );

	// sort and arrange the hosts, depending on the configuration
	// values set.

	int       k;
	uint16_t  idx1, idx2, tmp;

	// host idx entries with 0xFFFF mean end-of-the-line
	memset( &inst->host_idx_order , 0xFF , sizeof( inst->host_idx_order ) );

	// populate the host order, in order, first.
	for ( k = 0 ; k < inst->num_hosts ; k++ )
	{
		logh( inst , LOG_DEBUG , &inst->hosts[ k ] , "host_order(): running through host: " );
		logm( inst , LOG_DEBUG , "host_order(): setting inst->host_idx_order[%d] = %d" , k , k );
		inst->host_idx_order[k] = k;
	}
	inst->pos_order_idx = 0;

	// if we're asked to do a bit of shuffling, then there's some more work
	// to be done.
	if ( inst->host_order == SAAS_HOST_ORDER_RANDOM )
	{
		// randomly place the host indexes around
		logm( inst , LOG_DEBUG , "host_order(): shuffling host order" );
		srand( time( NULL ) );
		for ( k = 0 ; k < inst->num_hosts * 2 ; k++ )
		{
			// spawn two random indexes
			idx1 = rand() % inst->num_hosts;
			idx2 = rand() % inst->num_hosts;

			// do a switch-er-oo at the two different indexes
			tmp = inst->host_idx_order[idx1];
			inst->host_idx_order[idx1] = inst->host_idx_order[idx2];
			inst->host_idx_order[idx2] = tmp;
		}
	}

	for ( k = 0 ; k < inst->num_hosts ; k++ )
		logm( inst , LOG_DEBUG , "host_order():  idx %d -> host %d" , k , inst->host_idx_order[ k ] );

	logm( inst , LOG_DEBUG , "host_order(): finished" );
}



saas_host_t *host_get_next( saas_t *inst )
{
	// host_get_next(): get the next host in the order list prepared.
	logm( inst , LOG_DEBUG , "host_get_next(): starting" );

	saas_host_t         *host           = NULL;
	struct addrinfo     *dns_orig_ret   = NULL, *dns_ret = NULL;
	struct sockaddr_in  *ipv4_st        = NULL;
	struct sockaddr_in6 *ipv6_st        = NULL;
	int                  dns_lookup_ret = 0;
	uint32_t             resolved       = 0;
	char                 buf[128];


	// the loop surrounds the off chance that the current host we're giving back is IPv4/IPv6 and we've disallowed IPv4/IPv6.
	while ( inst->pos_order_idx <= inst->num_hosts )
	{
		logm( inst , LOG_DEBUG , "host_get_next(): looking through hosts list (up to host %d of %d) ..." , inst->pos_order_idx+1 , inst->num_hosts );

		// if no hosts are left to process, return NULL.  These two conditions should usually fire at the same
		// time... if not, there's a bug somewhere in this file.
		if ( inst->host_idx_order[ inst->pos_order_idx ] == 0xFFFF || inst->pos_order_idx >= inst->num_hosts )
		{
			logm( inst , LOG_DEBUG , "host_get_next(): no more hosts. finished." );
			return NULL;
		}

		// make a pointer to the host record we're about to give back.
		logm( inst , LOG_DEBUG , "host_get_next(): num_hosts = %d" , inst->num_hosts );
		logm( inst , LOG_DEBUG , "host_get_next(): pos_order_idx = %d" , inst->pos_order_idx );
		logm( inst , LOG_DEBUG , "host_get_next(): host_idx = %x" , inst->host_idx_order[ inst->pos_order_idx ] );
		host = &inst->hosts[ inst->host_idx_order[ inst->pos_order_idx ] ];


		// if the host we're about to give back is an unexpanded DNS host, we need to resolve the DNS name into an IP.  If there
		// was more than one IP returned, and we were configured to try all IPs, create additional hosts entries for the other
		// IPs, and then cut them in line in the hosts idx order, so that they are tried next.
		if ( host->type == SAAS_HOST_TYPE_IPV4 && inst->use_ipv4 == 0 )
		{
			// IPv4 addresses not allowed. Try the next host.
			logh( inst , LOG_NOTICE , host , "host_get_next(): conf file has an IPv4 host, but use_ipv4 is disabled: " );
			inst->pos_order_idx++;
			continue;
		}
		else if ( host->type == SAAS_HOST_TYPE_IPV4 && inst->use_ipv4 == 1 )
		{
			// IPv4 addresses are allowed.
			logh( inst , LOG_NOTICE , host , "host_get_next(): using IPv4 host: " );
			inst->pos_order_idx++;
			return host;
		}
		else if ( host->type == SAAS_HOST_TYPE_IPV6 && inst->use_ipv6 == 0 )
		{
			// IPv6 addresses not allowed. Try the next host.
			logh( inst , LOG_NOTICE , host , "host_get_next(): conf file has an IPv6 host, but use_ipv6 is disabled: " );
			inst->pos_order_idx++;
			continue;
		}
		else if ( host->type == SAAS_HOST_TYPE_IPV6 && inst->use_ipv6 == 1 )
		{
			// IPv6 addresses are allowed.
			logh( inst , LOG_DEBUG , host , "host_get_next(): using IPv6 host: " );
			inst->pos_order_idx++;
			return host;
		}
		else if ( host->type == SAAS_HOST_TYPE_DNS && ! ( host->flags & SAAS_HOST_FLAG_RESOLVED ) )
		{
			// looks like we need to resolve this one.
			logh( inst , LOG_DEBUG , host , "host_get_next(): got DNS host. Resolving... " );
			dns_lookup_ret = getaddrinfo( (const char *) &host->dns_name , NULL , NULL , &dns_orig_ret );
			if ( dns_lookup_ret != 0 )
			{
				// an error occured performing DNS lookup. report it and move on.
				logm( inst , LOG_WARNING , "error resolving DNS for `%s': (%d) %s" , host->dns_name , dns_lookup_ret , gai_strerror( dns_lookup_ret ) );
				inst->pos_order_idx++;
				continue;
			}

			// if we made it here, the DNS lookup was successful. time to trawl through the response.
			host->flags |= SAAS_HOST_FLAG_RESOLVED;
			resolved = 0;
			for ( dns_ret = dns_orig_ret ; dns_ret != NULL ; dns_ret = dns_ret->ai_next )
			{
				// don't include protocol-specific results.
				if ( dns_ret->ai_protocol != 0 )
					continue;
				if ( dns_ret->ai_family != AF_INET && dns_ret->ai_family != AF_INET6 )
					continue;

				// weed out any IPv4/IPv6 results if we don't want them.
				if ( dns_ret->ai_family == AF_INET && inst->use_ipv4 == 0 )
				{
					logm( inst , LOG_DEBUG , "host_get_next(): resolver gave an IPv4 address, but IPv4 is disabled. Skipping." );
					continue;
				}
				if ( dns_ret->ai_family == AF_INET6 && inst->use_ipv6 == 0 )
				{
					logm( inst , LOG_DEBUG , "host_get_next(): resolver gave an IPv6 address, but IPv6 is disabled. Skipping." );
					continue;
				}

				// everything looks good. let's drop them in.
				if ( dns_ret->ai_family == AF_INET )
				{
					ipv4_st = (struct sockaddr_in *) dns_ret->ai_addr;
					if ( inet_ntop( AF_INET , &ipv4_st->sin_addr , buf , sizeof( buf ) ) )
						logm( inst , LOG_DEBUG , "host_get_next(): resolved `%s' to IPv4: %s" , host->dns_name , buf );
					insert_host( inst , host , AF_INET , &ipv4_st->sin_addr );
					resolved++;

					// if we don't want to try all IPs, don't look at any more results!
					if ( inst->dns_try_all_ips == 0 )
						break;
				}
				else if ( dns_ret->ai_family == AF_INET6 )
				{
					ipv6_st = (struct sockaddr_in6 *) dns_ret->ai_addr;
					if ( inet_ntop( AF_INET6 , &ipv6_st->sin6_addr , buf , sizeof( buf ) ) )
						logm( inst , LOG_DEBUG , "host_get_next(): resolved `%s' to IPv6: %s" , host->dns_name , buf );
					insert_host( inst , host , AF_INET6 , &ipv6_st->sin6_addr );
					resolved++;

					// if we don't want to try all IPs, don't look at any more results!
					if ( inst->dns_try_all_ips == 0 )
						break;
				}
			}
			logm( inst , LOG_DEBUG , "host_get_next(): DNS resolution completed." );

			// because the existing DNS record stays in tact, and it's already been resolved
			// we need to advance on to the next host in the orer list, which will be an IP resolved
			// from the hostname above. The same behaviour applies if no IPs were resolved (just move
			// on to the next host in the list).

		} // if host type DNS & not RESOLVED

		inst->pos_order_idx++;
		continue;

	} // while

	// if we make it here, we didn't find a suitable candidate.
	logm( inst , LOG_DEBUG , "host_get_next(): no suitable host found. finished." );
	return NULL;
}


void insert_host( saas_t *inst , saas_host_t *parent_host , int addr_fam , void *addr )
{
	uint16_t idx = inst->num_hosts;
	logm( inst , LOG_DEBUG , "insert_host(): starting" );
	logm( inst , LOG_DEBUG , "insert_host(): currently there are %d hosts" , inst->num_hosts );

	dump_hosts( inst , LOG_DEBUG , "insert_host(): HOSTS BEFORE:" );

	if ( addr_fam == AF_INET )
	{
		memset( &inst->hosts[idx] , 0 , sizeof( saas_host_t ) );
		inst->hosts[idx].type  = SAAS_HOST_TYPE_IPV4;
		memcpy( &inst->hosts[idx].ipv4_addr , addr , 4 );
	}
	else if ( addr_fam == AF_INET6 )
	{
		memset( &inst->hosts[idx] , 0 , sizeof( saas_host_t ) );
		inst->hosts[idx].type  = SAAS_HOST_TYPE_IPV6;
		memcpy( &inst->hosts[idx].ipv6_addr , addr , 16 );
	}
	else
		return;

	inst->hosts[idx].flags = SAAS_HOST_FLAG_RESOLVED;
	strncpy( (char *) inst->hosts[idx].dns_name , (const char *) parent_host->dns_name , SAAS_MAX_DNS_HOST_LEN - 1 );
	inst->hosts[idx].port    = parent_host->port;
	inst->hosts[idx].timeout = parent_host->timeout;

	inst->num_hosts++;


	// now, we need to inject ourselves into the host_idx_order list so that we're
	// the next ones to be called.
	int k;
	for ( k = SAAS_MAX_NUM_HOSTS - 1 ; k > inst->pos_order_idx ; k-- )
		inst->host_idx_order[k+1] = inst->host_idx_order[k];
	k++;
	inst->host_idx_order[k] = idx;

	logm( inst , LOG_DEBUG , "insert_host(): now there are %d hosts" , inst->num_hosts );
	dump_hosts( inst , LOG_DEBUG , "insert_host(): HOSTS AFTER:" );
	logm( inst , LOG_DEBUG , "insert_host(): finished" );
}


void dump_hosts( saas_t *inst , uint8_t log_level , const char *line_prefix )
{
	int a, b;
	char buf[1024];

	if ( line_prefix == NULL)
		line_prefix = "";

	logm( inst , LOG_DEBUG , "dump_hosts(): starting" );
	logm( inst , log_level , "%snum_hosts = %d" , line_prefix , inst->num_hosts );
	logm( inst , log_level , "%shost entries:" , line_prefix );
	for ( a = 0 ; a < inst->num_hosts ; a++ )
	{
		memset( buf , 0 , sizeof( buf ) );

		if ( inst->hosts[a].type == SAAS_HOST_TYPE_DNS )
		{
			if ( inst->hosts[a].flags & SAAS_HOST_FLAG_RESOLVED )
				strcpy( buf , " (already resolved)" );
			else
				strcpy( buf , " (yet to be resolved)" );

			logm( inst , log_level , "%s    host[%d]: type=dns name=%s port=%d timeout=%dms%s" , line_prefix , a , inst->hosts[a].dns_name , inst->hosts[a].port , (inst->hosts[a].timeout > 0) ? inst->hosts[a].timeout : inst->timeout , buf );
		}
		else if ( inst->hosts[a].type == SAAS_HOST_TYPE_IPV4 )
		{
			if ( inst->hosts[a].flags & SAAS_HOST_FLAG_RESOLVED )
			{
				strcpy( buf , " (resolved from `" );
				strncat( buf , (char *) inst->hosts[a].dns_name , SAAS_MAX_DNS_HOST_LEN );
				strcat( buf , "')" );
			}

			logm( inst , log_level , "%s    host[%d]: type=ipv4 ip=%d.%d.%d.%d port=%d timeout=%dms%s" ,
				line_prefix , a ,
				inst->hosts[a].ipv4_addr[0] ,
				inst->hosts[a].ipv4_addr[1] ,
				inst->hosts[a].ipv4_addr[2] ,
				inst->hosts[a].ipv4_addr[3] ,
				inst->hosts[a].port ,
				(inst->hosts[a].timeout > 0) ? inst->hosts[a].timeout : inst->timeout ,
				buf
			);
		}
		else if ( inst->hosts[a].type == SAAS_HOST_TYPE_IPV6 )
		{
			if ( inst->hosts[a].flags & SAAS_HOST_FLAG_RESOLVED )
			{
				strcpy( buf , " (resolved from `" );
				strncat( buf , (char *) inst->hosts[a].dns_name , SAAS_MAX_DNS_HOST_LEN );
				strcat( buf , "')" );
			}

			logm( inst , log_level , "%s    host[%d]: type=ipv6 ip=%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x port=%d timeout=%dms%s" ,
				line_prefix , a ,
				inst->hosts[a].ipv6_addr[ 0] ,
				inst->hosts[a].ipv6_addr[ 1] ,
				inst->hosts[a].ipv6_addr[ 2] ,
				inst->hosts[a].ipv6_addr[ 3] ,
				inst->hosts[a].ipv6_addr[ 4] ,
				inst->hosts[a].ipv6_addr[ 5] ,
				inst->hosts[a].ipv6_addr[ 6] ,
				inst->hosts[a].ipv6_addr[ 7] ,
				inst->hosts[a].ipv6_addr[ 8] ,
				inst->hosts[a].ipv6_addr[ 9] ,
				inst->hosts[a].ipv6_addr[10] ,
				inst->hosts[a].ipv6_addr[11] ,
				inst->hosts[a].ipv6_addr[12] ,
				inst->hosts[a].ipv6_addr[13] ,
				inst->hosts[a].ipv6_addr[14] ,
				inst->hosts[a].ipv6_addr[15] ,
				inst->hosts[a].port ,
				(inst->hosts[a].timeout > 0) ? inst->hosts[a].timeout : inst->timeout ,
				buf
			);
		}
	}

	// run through the host order.
	logm( inst , log_level , "%shost order:" , line_prefix );
	for ( b = 0 ; b < inst->num_hosts ; b++ )
	{
		a = inst->host_idx_order[b];
		memset( buf , 0 , sizeof( buf ) );

		if ( inst->hosts[a].type == SAAS_HOST_TYPE_DNS )
		{
			if ( inst->hosts[a].flags & SAAS_HOST_FLAG_RESOLVED )
				strcpy( buf , " (already resolved)" );
			else
				strcpy( buf , " (yet to be resolved)" );

			logm( inst , log_level , "%s    host[%d]: type=dns name=%s port=%d timeout=%dms%s" , line_prefix , a , inst->hosts[a].dns_name , inst->hosts[a].port , (inst->hosts[a].timeout > 0) ? inst->hosts[a].timeout : inst->timeout , buf );
		}
		else if ( inst->hosts[a].type == SAAS_HOST_TYPE_IPV4 )
		{
			if ( inst->hosts[a].flags & SAAS_HOST_FLAG_RESOLVED )
			{
				strcpy( buf , " (resolved from `" );
				strncat( buf , (char *) inst->hosts[a].dns_name , SAAS_MAX_DNS_HOST_LEN );
				strcat( buf , "')" );
			}

			logm( inst , log_level , "%s    host[%d]: type=ipv4 ip=%d.%d.%d.%d port=%d timeout=%dms%s" ,
				line_prefix , a ,
				inst->hosts[a].ipv4_addr[0] ,
				inst->hosts[a].ipv4_addr[1] ,
				inst->hosts[a].ipv4_addr[2] ,
				inst->hosts[a].ipv4_addr[3] ,
				inst->hosts[a].port ,
				(inst->hosts[a].timeout > 0) ? inst->hosts[a].timeout : inst->timeout ,
				buf
			);
		}
		else if ( inst->hosts[a].type == SAAS_HOST_TYPE_IPV6 )
		{
			if ( inst->hosts[a].flags & SAAS_HOST_FLAG_RESOLVED )
			{
				strcpy( buf , " (resolved from `" );
				strncat( buf , (char *) inst->hosts[a].dns_name , SAAS_MAX_DNS_HOST_LEN );
				strcat( buf , "')" );
			}

			logm( inst , log_level , "%s    host[%d]: type=ipv6 ip=%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x:%02x%02x port=%d timeout=%dms%s" ,
				line_prefix , a ,
				inst->hosts[a].ipv6_addr[ 0] ,
				inst->hosts[a].ipv6_addr[ 1] ,
				inst->hosts[a].ipv6_addr[ 2] ,
				inst->hosts[a].ipv6_addr[ 3] ,
				inst->hosts[a].ipv6_addr[ 4] ,
				inst->hosts[a].ipv6_addr[ 5] ,
				inst->hosts[a].ipv6_addr[ 6] ,
				inst->hosts[a].ipv6_addr[ 7] ,
				inst->hosts[a].ipv6_addr[ 8] ,
				inst->hosts[a].ipv6_addr[ 9] ,
				inst->hosts[a].ipv6_addr[10] ,
				inst->hosts[a].ipv6_addr[11] ,
				inst->hosts[a].ipv6_addr[12] ,
				inst->hosts[a].ipv6_addr[13] ,
				inst->hosts[a].ipv6_addr[14] ,
				inst->hosts[a].ipv6_addr[15] ,
				inst->hosts[a].port ,
				(inst->hosts[a].timeout > 0) ? inst->hosts[a].timeout : inst->timeout ,
				buf
			);
		}
	}

	logm( inst , LOG_DEBUG , "dump_hosts(): finished" );
}


